Kotlin 在 Android 開發中最大的特色之一,就是 Coroutine(協程)。相較於 Java 傳統 Thread,不僅程式更容易閱讀,也能大幅降低 Callback Hell(回呼地獄)及執行緒管理的複雜度。
今天將透過完整範例,一次介紹:
⸻
為什麼需要 Coroutine?
在沒有 Coroutine 的年代,如果要執行:
通常都需要建立新的 Thread。
概念如下:
Main Thread
├── Thread A
├── Thread B
└── Thread C
Thread 建立成本高,而且大量 Thread 會造成系統負擔。
因此 Kotlin 推出了 Coroutine(協程)。
Coroutine 並不是 Thread,而是一個可以被暫停(Suspend)、恢復(Resume)的工作,因此能在少量 Thread 上同時執行大量任務。
⸻
CoroutineScope:協程的工作範圍
所有 Coroutine 都必須依附在某個 CoroutineScope。
今天的程式首先建立第一個 Scope:
var scope1 = CoroutineScope(Dispatchers.Default)
這代表:
之後即可透過 launch 建立 Coroutine。
scope1.launch {
}
⸻
launch:建立第一個 Coroutine
範例:
scope1.launch {
repeat(3) {
println("第一個 Scope - 協程:$it")
delay(300)
}
}
程式會輸出:
第一個 Scope - 協程:0
第一個 Scope - 協程:1
第一個 Scope - 協程:2
其中:
delay(300)
代表暫停 300 毫秒。
需要注意的是,delay() 並不會阻塞 Thread,而只是暫停目前 Coroutine,因此其他 Coroutine 仍可繼續執行。
⸻
suspend Function
另一個 Coroutine:
val job = CoroutineScope(Dispatchers.Default)
job.launch {
sFun()
}
其中:
suspend fun sFun() {
println("B")
delay(500)
println("C")
delay(500)
println("D")
delay(500)
println("E")
delay(500)
println("F")
}
suspend 函式只能在 Coroutine 中呼叫。
一般函式如果直接呼叫:
fun main() {
sFun()
}
將無法編譯。
因此 suspend 可以理解成:
「只能在 Coroutine 世界中執行的函式。」
⸻
delay 與 Thread.sleep 的差異
很多初學者容易把這兩者混淆。
Thread.sleep()
Thread.sleep(1000)
會阻塞整個 Thread。
也就是:
Thread
│
├── sleep
│
└── 全部停止
而 Coroutine:
delay(1000)
只是讓目前 Coroutine 暫停。
Thread 可以去執行其他 Coroutine。
因此 Android 官方建議:
Coroutine 一律使用 delay(),不要使用 Thread.sleep()。
⸻
主執行緒仍然持續工作
主程式:
for (count in 1..10) {
println("count = $count")
Thread.sleep(400)
}
輸出會交錯出現:
count = 1
第一個 Scope - 協程:0
count = 2
B
第一個 Scope - 協程:1
count = 3
C
代表:
正在同時執行。
這也是非同步程式設計最大的特色。
⸻
SupervisorJob:避免一個失敗拖垮全部
今天最重要的新觀念就是:
SupervisorJob。
建立方式:
scope1 = CoroutineScope(
Dispatchers.Default +
SupervisorJob()
)
接著建立兩個 Coroutine。
第一個:
scope1.launch {
delay(100)
println("Job 1")
error("job 1 fail")
}
第二個:
scope1.launch {
delay(200)
println("job 2 is ok")
}
如果沒有 SupervisorJob:
Job1 發生 Exception
↓
Job2 被取消
加入 SupervisorJob 後:
Job1 發生 Exception
↓
Job2 繼續執行
↓
job 2 is ok
也就是:
兄弟工作彼此獨立。
一個 Coroutine 失敗,不會影響其他 Coroutine。
Android 開發非常常見,例如:
其中圖片下載失敗,不應讓聊天室整個停止。
SupervisorJob 就是為了解決這個問題。
⸻
async:有回傳值的 Coroutine
除了 launch 之外,Coroutine 還提供:
async
例如:
val value1 = scope1.async {
delay(100)
println("async job 1")
100
}
val value2 = scope1.async {
delay(200)
println("async job 2")
200
}
與 launch 最大差別:
launch async
沒有回傳值 有回傳值
回傳 Job 回傳 Deferred
⸻
Deferred 與 await()
如果直接輸出:
println(value1)
得到的是:
DeferredCoroutine{Completed}
真正的資料必須使用:
println(value1.await())
輸出:
100
如果兩個都取得:
println(
"value1 = ${value1.await()}, value2 = ${value2.await()}"
)
輸出:
value1 = 100
value2 = 200
甚至可以直接計算:
val sum =
value1.await() +
value2.await()
println(sum)
輸出:
300
Android 常利用 async 同時下載多筆 API 資料,再一起更新畫面,大幅縮短等待時間。
⸻
Kotlin File 檔案操作
今天的範例也介紹了 Kotlin 的 File API。
建立資料夾:
val dir = File("c:\kotlin")
dir.mkdir()
建立檔案:
val file =
File("c:\kotlin\data1.txt")
寫入文字:
file.writeText("Hello Kotlin")
追加內容:
file.appendText("\nGood Morning")
讀取全部內容:
val text =
file.readText()
println(text)
逐行讀取:
val lines =
file.readLines()
for (line in lines) {
println(line)
}
也可以使用 FileWriter:
FileWriter(
"data2.txt",
false
).use {
it.write("Hello")
}
.use {} 等同 Java 的 try-with-resources,可以自動關閉檔案,避免資源洩漏。
⸻
Coroutine 在 Android 的應用
目前 Android 幾乎所有背景工作都使用 Coroutine,例如:
可以說,只要會 Android,就一定會接觸 Coroutine。
⸻
本日重點整理
✅ Coroutine 比 Thread 更輕量。
✅ CoroutineScope 管理 Coroutine 的生命週期。
✅ launch 適合沒有回傳值的背景工作。
✅ suspend 函式只能在 Coroutine 中呼叫。
✅ delay() 不會阻塞 Thread。
✅ Thread.sleep() 會阻塞整個 Thread。
✅ SupervisorJob 可避免一個 Coroutine 失敗影響其他 Coroutine。
✅ async 會回傳 Deferred。
✅ await() 用來取得 async 執行完成後的結果。
✅ Kotlin File API 提供簡潔的檔案讀寫方式,適合日常開發使用。
⸻
本日程式執行流程
Main Thread
│
├── CoroutineScope(scope1)
│ ├── launch
│ ├── launch
│ ├── async value1
│ └── async value2
│
├── CoroutineScope(job)
│ └── suspend sFun()
│
└── Main Thread 持續執行 count=1~10
透過這個範例,我們已經學會 Kotlin Coroutine 最核心的非同步設計模式,也是 Android 現代開發不可或缺的重要基礎。
⸻
下一篇預告
下一篇將介紹 Flow 與 StateFlow,了解 Kotlin 如何透過資料流(Flow)處理連續事件,以及如何在 Android 與 Jetpack Compose 中建立響應式(Reactive)UI,讓畫面能隨資料變化自動更新。
⸻
參考資料